﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading;

namespace Borg
{
    public sealed class Dispatcher : MarshalByRefObject, IDispatcher
    {
        private readonly object _monitor = new object();
        private readonly Queue<DispatcherOperation> _operations = new Queue<DispatcherOperation>();

        private bool _disposing;
        private bool _disposed;

        public event DispatcherEventHandler DisposeStarted;
        public event DispatcherEventHandler DisposeFinished;
        public event UnhandledExceptionEventHandler UnhandledException;

        private readonly IInvoker _invoker;
        private readonly Thread _worker;

        public bool IsDisposed
        {
            get { return _disposed; }
        }

        internal IInvoker Invoker
        {
            get { return _invoker; }
        }

        public Dispatcher()
            : this(new DefaultInvoker())
        { }

        public Dispatcher(IInvoker invoker)
        {
            if (invoker == null)
                throw new ArgumentNullException("invoker");
            
            _invoker = invoker;
            invoker.UnexpectedException += OnUnhandledException;

            _worker = new Thread(Run) { Name = "Runtime" };

            _worker.SetApartmentState(ApartmentState.STA);
            _worker.Start();
        }

        public void Dispose()
        {
            if (CheckAccess())
                throw new DeadlockException();
            
            BeginDispose();
            _worker.Join();
        }

        public void BeginDispose()
        {
            lock (_monitor)
            {
                if (_disposed || _disposing)
                    return;

                _disposing = true;
                BeginInvoke(new Handler(InternalDispose), new object[0]);
            }
        }

        private void InternalDispose()
        {
            Debug.Assert(CheckAccess());
            OnDisposingStarted();

            lock (_monitor)
            {
                BeginInvoke(new Handler(OnDisposingFinished), new object[0]);
                BeginInvoke(new Handler(((IDisposable)Invoker).Dispose), new object[0]);
                
                _disposed = true;
                _disposing = false;

                Monitor.PulseAll(_monitor);
            }
        }

        private void OnDisposingStarted()
        {
            Debug.Assert(CheckAccess());

            if (DisposeStarted == null)
                return;
            
            DisposeStarted(this);
        }

        private void OnDisposingFinished()
        {
            Debug.Assert(CheckAccess());

            if (DisposeFinished == null)
                return;
            
            DisposeFinished(this);
        }

        private void Run()
        {
            Handler aborter = _worker.Abort;

            while (true)
            {
                DispatcherOperation dispatcherOperation;

                lock (_monitor)
                {
                    while (_operations.Count <= 0)
                    {
                        if (_disposed)
                            return;

                        Monitor.Wait(_monitor);
                    }

                    dispatcherOperation = _operations.Dequeue();
                }

                dispatcherOperation.Invoke(aborter);
                dispatcherOperation.OnUnhandledException -= OnUnhandledException;
                
                dispatcherOperation = null;
            }
        }

        public void VerifyAccess()
        {
            if (!CheckAccess())
                throw new InvalidOperationException("Current thread cannot access this object.");
        }

        public bool CheckAccess()
        {
            return Invoker.CheckAccess(this);
        }

        public bool IsWorker(Thread other)
        {
            return _worker.Equals(other);
        }

        public bool IsIdle()
        {
            lock (_monitor)
                return _operations.Count == 0;
        }

        public object Invoke(Delegate @delegate, params object[] @params)
        {
            if (CheckAccess())
                return @delegate.DynamicInvoke(@params);

            var dispatcherOperation = BeginInvoke(false, @delegate, @params);
            
            switch (dispatcherOperation.Wait())
            {
                case DispatcherOperationStatus.Completed:
                    return dispatcherOperation.Result;
                
                case DispatcherOperationStatus.Aborted:
                    throw new TargetInvocationException(dispatcherOperation.UnhandledException);
                
                default:
                    Debug.Assert(false);
                    throw new InvalidOperationException();
            }
        }

        public IDispatcherOperation BeginInvoke(Delegate @delegate, params object[] @params)
        {
            return BeginInvoke(true, @delegate, @params);
        }

        private DispatcherOperation BeginInvoke(bool fireOnUnhandledExceptionEvent, Delegate @delegate, params object[] @params)
        {
            lock (_monitor)
            {
                VerifyNotDisposed();
                var dispatcher = new DispatcherOperation(this, @delegate, @params);

                if (fireOnUnhandledExceptionEvent)
                    dispatcher.OnUnhandledException += OnUnhandledException;
                
                _operations.Enqueue(dispatcher);
                Monitor.PulseAll(_monitor);

                return dispatcher;
            }
        }

        private void VerifyNotDisposed()
        {
            if (IsDisposed)
                throw new ObjectDisposedException("Dispatcher already disposed.");
        }

        private void OnUnhandledException(IInvoker sender, Exception exception)
        {
            OnUnhandledException(exception);
        }

        private void OnUnhandledException(IDispatcherOperation sender, Exception exception)
        {
            OnUnhandledException(exception);
        }

        private void OnUnhandledException(Exception exception)
        {
            OnUnhandledException(this, new DispatcherExceptionEventArgs(exception));
        }

        private void OnUnhandledException(IDispatcher sender, DispatcherExceptionEventArgs args)
        {
            if (UnhandledException == null)
                return;

            if (CheckAccess())
                UnhandledException(sender, args);
            else
                _invoker.Invoke(new UnhandledExceptionEventHandler(
                    UnhandledException.Invoke), (object)sender, (object)args);
        }
    }
}
